.NET6 C#, LineBot, Line Messaging API, C#, dotnet core
今天這篇的內容會帶大家完成一個基本的 Line Pay 交易流程,內容包含前端與後端。
以下是 Line Pay 大致的付款流程
每筆 Line Pay 交易有以下狀態 :
上述第 3 點的狀態可另外再將確認交易與請款分成兩步驟,變成以下 4 種狀態 :
而我們今天建立的基本交易流程以 1、2 為主,剩下的我們下一篇再說明~
接下來,要使用 Line Pay API,則必須有 Line Pay 商家的帳號,並在其中取得 Channel Id & Channe Secret Key,在開發測試的過程中,可以使用 Line 提供的 sandbox 的環境,接下來先去註冊一個 snadbox 帳號吧。(* 正式的LINE PAY帳號需要跟官方申請,需要比較多的文件佐證,詳細就留給各位參考官方的文件囉)
跟著以下步驟註冊並取得 channel id & channel secret key
註冊 sandbox 帳號 Line Pay Sandbox 註冊連結
註冊成功後,會在 email 收到 sandbox 的帳號密碼。
前往 Line Pay 商家頁面使用剛剛收到的帳密登入。 Line Pay 商家
4.登入成功後到 管理連結金鑰 目錄中,按下查詢並取得驗證碼驗證成功後即可取得 channel id & channel secret key (此為安全機制,並不是每次都需要重新驗證查詢取得)
取得 channel id 與 channel secret key 後就可以開始串接 Line Pay API 了~
下圖是我們這次要建立的流程圖,包含前端與後端。
所以在建立 API 前先建立一個 SignatureProvider
* 若對 HMAC-SHA256 有興趣,介紹可以參考這篇 連結 *
Line Pay 使用 HMAC-SHA256 演算法做簽章,又根據要打的 API 不同的 Http Method 有不同的訊息內容要求。
看起來雖然很複雜,但 .NET 有提供 HMAC-SHA256 的方法,直接使用即可。
using System.Security.Cryptography;
namespace LineBotMessage.Providers
{
public static class SignatureProvider
{
public static string HMACSHA256(string key, string message)
{
System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
//取的 key byte 值
byte[] keyByte = encoding.GetBytes(key);
// 取得 key 對應的 hmacsha256
HMACSHA256 hmacsha256 = new HMACSHA256(keyByte);
// 取的 message byte 值
byte[] messageBytes = encoding.GetBytes(message);
// 將 message 使用 key 值對應的 hamcsha256 作 hash 簽章
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Web;
using LineBotMessage.Dtos;
using LineBotMessage.Providers;
namespace LineBotMessage.Domain
{
public class LinePayService
{
public LinePayService()
{
client = new HttpClient();
_jsonProvider = new JsonProvider();
}
private readonly string channelId = "{你的 LinePay 商家 Channel ID}";
private readonly string channelSecretKey = "{你的 Line Pay 商家 Channel Secret Key}";
private readonly string linePayBaseApiUrl = "https://sandbox-api-pay.line.me";
private static HttpClient client;
private readonly JsonProvider _jsonProvider;
}
}
(文件內容太長,就不截圖了,class 將文件上所有屬性都建立起來了,但這邊測試不會全部用到,各位事後自行測試吧!)
namespace LineBotMessage.Dtos
{
public class PaymentRequestDto
{
public int Amount { get; set; }
public string Currency { get; set; }
public string OrderId { get; set; }
public List<PackageDto> Packages { get; set; }
public RedirectUrlsDto RedirectUrls { get; set; }
public RequestOptionDto? Options { get; set; }
}
public class PackageDto
{
public string Id { get; set; }
public int Amount { get; set; }
public string Name { get; set; }
public List<LinePayProductDto> Products { get; set; }
public int? UserFee { get; set; }
}
public class LinePayProductDto
{
public string Name { get; set; }
public int Quantity { get; set; }
public int Price { get; set; }
public string? Id { get; set; }
public string? ImageUrl { get; set; }
public int? OriginalPrice { get; set; }
}
public class RedirectUrlsDto
{
public string ConfirmUrl { get; set; }
public string CancelUrl { get; set; }
public string? AppPackageName { get; set; }
public string? ConfirmUrlType { get; set; }
}
public class RequestOptionDto
{
public PaymentOptionDto? Payment { get; set; }
public DisplpyOptionDto? Displpy { get; set; }
public ShippingOptionDto? Shipping { get; set; }
public ExtraOptionsDto? Extra { get; set; }
}
public class PaymentOptionDto
{
public bool? Capture { get; set; }
public string? PayType { get; set; }
}
public class DisplpyOptionDto
{
public string? Local { get; set; }
public bool? CheckConfirmUrlBrowser { get; set; }
}
public class ShippingOptionDto
{
public string? Type { get; set; }
public int FeeAmount { get; set; }
public string? FeeInquiryUrl { get; set; }
public string? FeeInquiryType { get; set; }
public ShippingAddressDto? Address { get; set; }
}
public class ShippingAddressDto
{
public string? Country { get; set; }
public string? PostalCode { get; set; }
public string? State { get; set; }
public string? City { get; set; }
public string? Detail { get; set; }
public string? Optional { get; set; }
public ShippingAddressRecipientDto Recipient { get; set; }
}
public class ShippingAddressRecipientDto
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? FirstNameOptional { get; set; }
public string? LastNameOptional { get; set; }
public string? Email { get; set; }
public string? PhoneNo { get; set; }
public string? Type { get; set; }
}
public class ExtraOptionsDto
{
public string? BranchName { get; set; }
public string? BranchId { get; set; }
}
}
namespace LineBotMessage.Dtos
{
public class PaymentConfirmResponseDto
{
public string ReturnCode { get; set; }
public string ReturnMessage { get; set; }
public ConfirmResponseInfoDto Info { get; set; }
}
public class ConfirmResponseInfoDto
{
public string OrderId { get; set; }
public long TransactionId { get; set; }
public string AuthorizationExpireDate { get; set; }
public string RegKey { get; set; }
public ConfirmResponsePayInfoDto[] PayInfo { get; set; }
}
public class ConfirmResponsePayInfoDto
{
public string Method { get; set; }
public int Amount { get; set; }
public string CreditCardNickname { get; set; }
public string CreditCardBrand { get; set; }
public string MaskedCreditCardNumber { get; set; }
public ConfirmResponsePackageDto[] Packages { get; set; }
public ConfirmResponseShippingOptionsDto Shipping { get; set; }
}
public class ConfirmResponsePackageDto
{
public string Id { get; set; }
public int Amount { get; set; }
public int UserFeeAmount { get; set; }
}
public class ConfirmResponseShippingOptionsDto
{
public string MethodId { get; set; }
public int FeeAmount { get; set; }
public ShippingAddressDto Address { get; set; }
}
}
// 送出建立交易請求至 Line Pay Server
public async Task<PaymentResponseDto> SendPaymentRequest(PaymentRequestDto dto)
{
var json = _jsonProvider.Serialize(dto);
// 產生 GUID Nonce
var nonce = Guid.NewGuid().ToString();
// 要放入 signature 中的 requestUrl
var requestUrl = "/v3/payments/request";
//使用 channelSecretKey & requestUrl & jsonBody & nonce 做簽章
var signature = SignatureProvider.HMACSHA256(channelSecretKey, channelSecretKey + requestUrl + json + nonce);
var request = new HttpRequestMessage(HttpMethod.Post, linePayBaseApiUrl + requestUrl)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
// 帶入 Headers
client.DefaultRequestHeaders.Add("X-LINE-ChannelId", channelId);
client.DefaultRequestHeaders.Add("X-LINE-Authorization-Nonce", nonce);
client.DefaultRequestHeaders.Add("X-LINE-Authorization", signature);
var response = await client.SendAsync(request);
var linePayResponse = _jsonProvider.Deserialize<PaymentResponseDto>(await response.Content.ReadAsStringAsync());
Console.WriteLine(nonce);
Console.WriteLine(signature);
return linePayResponse;
}
namespace LineBotMessage.Dtos
{
public class PaymentConfirmDto
{
public int Amount { get; set; }
public string Currency { get; set; }
}
}
namespace LineBotMessage.Dtos
{
public class PaymentResponseDto
{
public string ReturnCode { get; set; }
public string ReturnMessage { get; set; }
public ResponseInfoDto Info { get; set; }
}
public class ResponseInfoDto
{
public ResponsePaymentUrlDto PaymentUrl { get; set; }
public long TransactionId { get; set; }
public string PaymentAccessToken { get; set; }
}
public class ResponsePaymentUrlDto
{
public string Web { get; set; }
public string App { get; set; }
}
}
// 取得 transactionId 後進行確認交易
public async Task<PaymentConfirmResponseDto> ConfirmPayment(string transactionId, string orderId, PaymentConfirmDto dto) //加上 OrderId 去找資料
{
var json = _jsonProvider.Serialize(dto);
var nonce = Guid.NewGuid().ToString();
var requestUrl = string.Format("/v3/payments/{0}/confirm", transactionId);
var signature = SignatureProvider.HMACSHA256(channelSecretKey, channelSecretKey + requestUrl + json + nonce);
var request = new HttpRequestMessage(HttpMethod.Post, String.Format(linePayBaseApiUrl + requestUrl, transactionId))
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
client.DefaultRequestHeaders.Add("X-LINE-ChannelId", channelId);
client.DefaultRequestHeaders.Add("X-LINE-Authorization-Nonce", nonce);
client.DefaultRequestHeaders.Add("X-LINE-Authorization", signature);
var response = await client.SendAsync(request);
var responseDto = _jsonProvider.Deserialize<PaymentConfirmResponseDto>(await response.Content.ReadAsStringAsync());
return responseDto;
}
當使用者在交易 Processing 時取消交易,LinePay 會將該資訊透過最開始建立交易時帶入的 CancelUrl 回傳通知,這邊就先開個接口放著就好。
public async void TransactionCancel(string transactionId)
{
//使用者取消交易則會到這裏。
Console.WriteLine($"訂單 {transactionId} 已取消");
}
using LineBotMessage.Dtos;
using LineBotMessage.Domain;
using Microsoft.AspNetCore.Mvc;
namespace LineBotMessage.Controllers
{
[ApiController]
[Route("api/[Controller]")]
public class LinePayController : ControllerBase
{
private readonly LinePayService _linePayService;
public LinePayController()
{
_linePayService = new LinePayService();
}
[HttpPost("Create")]
public async Task<PaymentResponseDto> CreatePayment(PaymentRequestDto dto)
{
return await _linePayService.SendPaymentRequest(dto);
}
[HttpPost("Confirm")]
public async Task<PaymentConfirmResponseDto> ConfirmPayment([FromQuery] string transactionId, [FromQuery] string orderId, PaymentConfirmDto dto )
{
return await _linePayService.ConfirmPayment(transactionId, orderId,dto);
}
[HttpGet("Cancel")]
public async void CancelTransaction([FromQuery] string transactionId)
{
_linePayService.TransactionCancel(transactionId);
}
}
}
本篇前端有兩個頁面,分別為 products.html & confirm.html,各自代表 :
<!DOCTYPE html>
<html lang="en">
<head>
<title>2022 iThome 鐵人賽 - 讓 C# 也能很 Social</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- jquery CDN include -->
<script src="https://code.jquery.com/jquery-3.6.1.min.js"
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
<!-- CSS include -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script>
let baseLoginPayUrl = 'https://localhost:8080/api/LinePay/';
function requestPayment() {
// 交易訂單假資料
payment = {
amount: 3998,
currency: "TWD",
orderId: Date.now().toString(), //使用 Timestamp 當作 orderId
packages: [
{
id: "20191011I001",
amount: 3998,
name: "測試",
products: [
{
name: "測試商品",
imageUrl: "https://static.accupass.com/org/2011051025162614811630.jpg",
quantity: 2,
price: 1999,
}
]
},
],
RedirectUrls: {
ConfirmUrl: "https://cccf-61-63-154-173.jp.ngrok.io/confirm.html",
CancelUrl: "https://c4f0-61-63-154-173.jp.ngrok.io/api/LinePay/Cancel",
},
};
// 送出 交易申請至商家 server
$.post({
url: baseLoginPayUrl + "Create",
dataType: "json",
contentType: "application/json",
data: JSON.stringify(payment),
success: (res) => {
window.location = res.info.paymentUrl.web;
},
error: (err) => {
console.log(err);
}
})
}
</script>
<!-- 最上方的 bar -->
<div class="topnav">
<a href="https://cccf-61-63-154-173.jp.ngrok.io/login.html">Line Login</a>
<a href="https://cccf-61-63-154-173.jp.ngrok.io/profile.html">User Profile</a>
<a href="https://cccf-61-63-154-173.jp.ngrok.io/products.html">Line Pay</a>
</div>
<!-- 商品畫面 -->
<center>
<table>
<thead>
<tr>
<th> 測試商品 </th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://static.accupass.com/org/2011051025162614811630.jpg"></td>
</tr>
<tr>
<td> 價格 : 1999 </td>
</tr>
<tr>
<td> 購買數量 : 2 </td>
</tr>
<tr>
<td style="text-align: right;"> 總金額 : 3998 </td>
</tr>
<tr>
<td align="center"><button onclick="requestPayment()"> Line Pay 付款</button></td>
</tr>
</tbody>
</table>
</center>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>2022 iThome 鐵人賽 - 讓 C# 也能很 Social</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- jquery CDN include -->
<script src="https://code.jquery.com/jquery-3.6.1.min.js"
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
<!-- CSS include -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<script>
let baseLoginPayUrl = 'https://localhost:8080/api/LinePay/';
let transactionId = "";
let orderId = "";
window.onload = () => {
// 取出 query parameter 中的 transactionId & orderId
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
transactionId = params.transactionId;
orderId = params.orderId;
}
function confirmPayment() {
// 交易訂單假資料
payment = {
amount: 3998,
currency: "TWD",
};
// 送出確認交易
$.post({
url: baseLoginPayUrl + `Confirm?transactionId=${transactionId}&orderId=${orderId}`,
dataType: "json",
contentType: "application/json",
data: JSON.stringify(payment),
success: (res) => {
$("#paymentStatus").text("交易狀態 : 成功")
console.log(res);
setTimeout(() => {
window.location = "https://cccf-61-63-154-173.jp.ngrok.io/products.html";
}, 2000);
},
error: (err) => {
console.log(err);
}
})
}
</script>
<!-- 最上方的 bar -->
<div class="topnav">
<a href="https://cccf-61-63-154-173.jp.ngrok.io/login.html">Line Login</a>
<a href="https://cccf-61-63-154-173.jp.ngrok.io/profile.html">User Profile</a>
<a href="https://cccf-61-63-154-173.jp.ngrok.io/products.html">Line Pay</a>
</div>
<center>
<table>
<thead>
<tr>
<th colspan="2"> 測試商品 </th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><img src="https://static.accupass.com/org/2011051025162614811630.jpg"></td>
</tr>
<tr>
<td colspan="2"> 價格 : 1999 </td>
</tr>
<tr>
<td colspan="2"> 購買數量 : 2 </td>
</tr>
<tr>
<td colspan="2" style="text-align: right;"> 總金額 : 3998 </td>
</tr>
<tr>
<td align="center" colspan="2"><button onclick="confirmPayment()"> 確認付款</button></td>
</tr>
</tbody>
</table>
<div class="Container">
<a id="paymentStatus">交易狀態 : 交易已授權,等待確認<a>
</div>
</center>
</body>
</html>
* 因為 ngrok 跨域請求問題的原因,所以前端的 request url 是打 localhost,因此測試只能在電腦上進行
商品結帳畫面,按下付款後跳轉至 Line Pay 登入畫面
Line Pay 登入畫面
Line Pay 使用者授權模擬畫面,按下付款鈕後即進入授權程序
Confrim 頁面,按下確認後則交易完成。
今天的的內容是只一個基本的付款流程,方便各位參考。
整個流程其實還可以有更多的變化,例如最開始提到的,一種是將確認交易與請款兩個狀態拆開來處理,第二種是可以將 confirm 這件事直接交給 server 負責(讓使用者可省略一步驟)。 除此之外,Line Pay 也有自動扣款的功能可以實作,相信透過今天的例子,大家應該對LINE PAY的使用及相關情境,能有多一點的了解。如果有什麼想多了解的,歡迎再留言讓我們知道囉 ~
下一篇會接續LINE PAY的主題,針對常見情境會用到的API來進行示範 ~
如果想要參考今天範例程式碼的部份,下面是 Git Repo 連結,方便大家參考。
前端 Branch - Day24
front-end page
Display 打錯,打成 Displpy ~
PaymentRequestDto.cs
`
public class RequestOptionDto
{
public PaymentOptionDto? Payment { get; set; }
public DisplpyOptionDto? Displpy { get; set; }
public ShippingOptionDto? Shipping { get; set; }
public ExtraOptionsDto? Extra { get; set; }
}
public class DisplpyOptionDto
{
public string? Local { get; set; }
public bool? CheckConfirmUrlBrowser { get; set; }
}
`